]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
89f58e3d4b53b9359869087eaddc88ef4fe64636
[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, urllib2, gobject, 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.endpoints import TCP4ClientEndpoint
51 from twisted.internet.error import DNSLookupError
52
53 from txsocksx.client import SOCKS5ClientEndpoint
54
55 import OpenSSL
56
57
58 class TryStableException(Exception):
59     pass
60
61
62 class TryDefaultMirrorException(Exception):
63     pass
64
65
66 class DownloadErrorException(Exception):
67     pass
68
69
70 class VerifyTorProjectCert(ClientContextFactory):
71
72     def __init__(self, torproject_pem):
73         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
74
75     def getContext(self, host, port):
76         ctx = ClientContextFactory.getContext(self)
77         ctx.set_verify_depth(0)
78         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
79         return ctx
80
81     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
82         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
83
84
85 class TBLCommon:
86
87     def __init__(self, tbl_version):
88         print _('Initializing Tor Browser Launcher')
89         self.tbl_version = tbl_version
90
91         # initialize the app
92         self.available_versions = {
93             'stable': _('Tor Browser Bundle - stable'),
94             'alpha': _('Tor Browser Bundle - alpha')
95         }
96         self.default_mirror = 'https://www.torproject.org/dist/'
97
98         self.discover_arch_lang()
99         self.build_paths()
100         self.mkdir(self.paths['data_dir'])
101         self.load_mirrors()
102         self.load_settings()
103         self.mkdir(self.paths['download_dir'])
104         self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
105         self.init_gnupg()
106
107         # allow buttons to have icons
108         try:
109             gtk_settings = gtk.settings_get_default()
110             gtk_settings.props.gtk_button_images = True
111         except:
112             pass
113
114     # discover the architecture and language
115     def discover_arch_lang(self):
116         # figure out the architecture
117         (sysname, nodename, release, version, machine) = os.uname()
118         self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
119
120         # figure out the language
121         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
122         default_locale = locale.getdefaultlocale()[0]
123         if default_locale is None:
124             self.language = 'en-US'
125         else:
126             self.language = default_locale.replace('_', '-')
127             if self.language not in available_languages:
128                 self.language = self.language.split('-')[0]
129                 if self.language not in available_languages:
130                     for l in available_languages:
131                         if l[0:2] == self.language:
132                             self.language = l
133             # if language isn't available, default to english
134             if self.language not in available_languages:
135                 self.language = 'en-US'
136
137     # build all relevant paths
138     def build_paths(self, tbb_version=None):
139         homedir = os.getenv('HOME')
140         if not homedir:
141             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
142             if not os.path.exists(homedir):
143                 try:
144                     os.mkdir(homedir, 0700)
145                 except:
146                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
147         if not os.access(homedir, os.W_OK):
148             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
149
150         tbb_data = '%s/.torbrowser' % homedir
151
152         if tbb_version:
153             # tarball filename
154             dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
155             if self.architecture == 'x86_64':
156                 arch = 'linux64'
157             else:
158                 arch = 'linux32'
159             tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
160
161             # tarball
162             self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+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
170             self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
171             self.paths['tarball_filename'] = tarball_filename
172
173         else:
174             self.paths = {
175                 'tbl_bin': '/usr/bin/torbrowser-launcher',
176                 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
177                 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
178                 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
179                 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
180                 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
181                 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
182                 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
183                                 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
184                 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
185                 'data_dir': tbb_data,
186                 'download_dir': tbb_data+'/download',
187                 'gnupg_homedir': tbb_data+'/gnupg_homedir',
188                 'settings_file': tbb_data+'/settings',
189                 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
190                 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
191                 'tbb': {
192                     'stable': {
193                         'dir': tbb_data+'/tbb/stable/'+self.architecture,
194                         'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
195                         'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Browser/firefox',
196                         'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
197                         'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
198                     },
199                     'alpha': {
200                         'dir': tbb_data+'/tbb/alpha/'+self.architecture,
201                         'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
202                         'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Browser/firefox',
203                         'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
204                         'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
205                     }
206                 }
207             }
208
209     # create a directory
210     def mkdir(self, path):
211         try:
212             if not os.path.exists(path):
213                 os.makedirs(path, 0700)
214                 return True
215         except:
216             print _("Cannot create directory {0}").format(path)
217             return False
218         if not os.access(path, os.W_OK):
219             print _("{0} is not writable").format(path)
220             return False
221         return True
222
223     # if gnupg_homedir isn't set up, set it up
224     def init_gnupg(self):
225         if not os.path.exists(self.paths['gnupg_homedir']):
226             print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
227             self.mkdir(self.paths['gnupg_homedir'])
228         self.import_keys()
229
230     # import gpg keys
231     def import_keys(self):
232         print _('Importing keys')
233         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()
234
235     # load mirrors
236     def load_mirrors(self):
237         self.mirrors = []
238         for srcfile in self.paths['mirrors_txt']:
239             if not os.path.exists(srcfile):
240                 print "Warning: can't load mirrors from %s" % srcfile
241                 continue
242             for mirror in open(srcfile, 'r').readlines():
243                 if mirror.strip() not in self.mirrors:
244                     self.mirrors.append(mirror.strip())
245
246     # load settings
247     def load_settings(self):
248         default_settings = {
249             'tbl_version': self.tbl_version,
250             'preferred': 'stable',
251             'installed_version': {
252                 'stable': False,
253                 'alpha': False
254             },
255             'latest_version': {
256                 'stable': '0',
257                 'alpha': '0'
258             },
259             'update_over_tor': True,
260             'check_for_updates': False,
261             'modem_sound': False,
262             'last_update_check_timestamp': 0,
263             'mirror': self.default_mirror
264         }
265
266         if os.path.isfile(self.paths['settings_file']):
267             settings = pickle.load(open(self.paths['settings_file']))
268             resave = False
269
270             # settings migrations
271             if settings['tbl_version'] == '0.0.1':
272                 print '0.0.1 migration'
273                 self.settings = default_settings
274                 self.settings['installed_version']['alpha'] = settings['installed_version']
275                 resave = True
276
277                 # move tbb alpha
278                 self.mkdir(self.paths['tbb']['alpha']['dir'])
279
280             # make sure settings file is up-to-date
281             for setting in default_settings:
282                 if setting not in settings:
283                     settings[setting] = default_settings[setting]
284                     resave = True
285
286             # make sure the version is current
287             if settings['tbl_version'] != self.tbl_version:
288                 settings['tbl_version'] = self.tbl_version
289                 resave = True
290
291             self.settings = settings
292             if resave:
293                 self.save_settings()
294
295         else:
296             self.settings = default_settings
297             self.save_settings()
298
299     # save settings
300     def save_settings(self):
301         pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
302         return True
303
304     # get the process id of a program
305     def get_pid(self, bin_path, python=False):
306         pid = None
307
308         for p in psutil.process_iter():
309             try:
310                 if p.pid != os.getpid():
311                     exe = None
312                     if python:
313                         if len(p.cmdline) > 1:
314                             if 'python' in p.cmdline[0]:
315                                 exe = p.cmdline[1]
316                     else:
317                         if len(p.cmdline) > 0:
318                             exe = p.cmdline[0]
319
320                     if exe == bin_path:
321                         pid = p.pid
322
323             except:
324                 pass
325
326         return pid
327
328     # bring program's x window to front
329     def bring_window_to_front(self, pid):
330         # figure out the window id
331         win_id = None
332         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
333         for line in p.stdout.readlines():
334             line_split = line.split()
335             cur_win_id = line_split[0]
336             cur_win_pid = int(line_split[2])
337             if cur_win_pid == pid:
338                 win_id = cur_win_id
339
340         # bring to front
341         if win_id:
342             subprocess.call(['wmctrl', '-i', '-a', win_id])
343
344
345 class TBLSettings:
346     def __init__(self, common):
347         print _('Starting settings dialog')
348         self.common = common
349
350         # set up the window
351         self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
352         self.window.set_title(_("Tor Browser Launcher Settings"))
353         self.window.set_icon_from_file(self.common.paths['icon_file'])
354         self.window.set_position(gtk.WIN_POS_CENTER)
355         self.window.set_border_width(10)
356         self.window.connect("delete_event", self.delete_event)
357         self.window.connect("destroy", self.destroy)
358
359         # build the rest of the UI
360         self.box = gtk.VBox(False, 10)
361         self.window.add(self.box)
362         self.box.show()
363
364         self.hbox = gtk.HBox(False, 10)
365         self.box.pack_start(self.hbox, True, True, 0)
366         self.hbox.show()
367
368         self.settings_box = gtk.VBox(False, 10)
369         self.hbox.pack_start(self.settings_box, True, True, 0)
370         self.settings_box.show()
371
372         self.labels_box = gtk.VBox(False, 10)
373         self.hbox.pack_start(self.labels_box, True, True, 0)
374         self.labels_box.show()
375
376         # preferred version
377         self.preferred_box = gtk.HBox(False, 10)
378         self.settings_box.pack_start(self.preferred_box, True, True, 0)
379         self.preferred_box.show()
380
381         self.preferred_label = gtk.Label(_('I prefer'))
382         self.preferred_label.set_line_wrap(True)
383         self.preferred_box.pack_start(self.preferred_label, True, True, 0)
384         self.preferred_label.show()
385
386         self.preferred_options = []
387         for i in self.common.available_versions:
388             self.preferred_options.append(self.common.available_versions[i])
389         self.preferred_options.sort()
390
391         self.preferred = gtk.combo_box_new_text()
392         for option in self.preferred_options:
393             self.preferred.append_text(option)
394         if self.common.settings['preferred'] in self.common.available_versions:
395             self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
396         else:
397             self.preferred.set_active(0)
398         self.preferred_box.pack_start(self.preferred, True, True, 0)
399         self.preferred.show()
400
401         # download over tor
402         # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
403         """
404         self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
405         self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
406         if self.common.settings['update_over_tor']:
407             self.tor_update_checkbox.set_active(True)
408         else:
409             self.tor_update_checkbox.set_active(False)
410         self.tor_update_checkbox.show()
411         """
412
413         # check for updates
414         self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
415         self.settings_box.pack_start(self.update_checkbox, True, True, 0)
416         if self.common.settings['check_for_updates']:
417             self.update_checkbox.set_active(True)
418         else:
419             self.update_checkbox.set_active(False)
420         self.update_checkbox.show()
421
422         # modem sound
423         self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
424         self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
425         if self.common.settings['modem_sound']:
426             self.modem_checkbox.set_active(True)
427         else:
428             self.modem_checkbox.set_active(False)
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         p = 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_sig':
797             print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
798             self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
799
800         elif task == 'download_tarball':
801             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
802             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
803
804         elif task == 'verify':
805             print _('Verifying signature')
806             self.verify()
807
808         elif task == 'extract':
809             print _('Extracting'), self.common.paths['tarball_filename']
810             self.extract()
811
812         elif task == 'run':
813             print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
814             self.run()
815
816         elif task == 'start_over':
817             print _('Starting download over again')
818             self.start_over()
819
820     def response_received(self, response):
821         class FileDownloader(Protocol):
822             def __init__(self, common, file, total, progress, done_cb):
823                 self.file = file
824                 self.total = total
825                 self.so_far = 0
826                 self.progress = progress
827                 self.all_done = done_cb
828
829                 if response.code != 200:
830                     try_stable = False
831
832                     if response.code == 404:
833                         if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
834                             try_stable = True
835
836                     if try_stable:
837                         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?"))
838                     else:
839                         if common.settings['mirror'] != common.default_mirror:
840                             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']))
841                         else:
842                             raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
843
844             def dataReceived(self, bytes):
845                 self.file.write(bytes)
846                 self.so_far += len(bytes)
847                 percent = float(self.so_far) / float(self.total)
848                 self.progress.set_fraction(percent)
849                 amount = float(self.so_far)
850                 units = "bytes"
851                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
852                     if amount > size:
853                         units = unit
854                         amount = amount / float(size)
855                         break
856
857                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
858
859             def connectionLost(self, reason):
860                 print _('Finished receiving body:'), reason.getErrorMessage()
861                 self.all_done(reason)
862
863         dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
864         response.deliverBody(dl)
865
866     def response_finished(self, msg):
867         if msg.check(ResponseDone):
868             self.file_download.close()
869             delattr(self, 'current_download_path')
870
871             # next task!
872             self.run_task()
873
874         else:
875             print "FINISHED", msg
876             ## FIXME handle errors
877
878     def download_error(self, f):
879         print _("Download error:"), f.value, type(f.value)
880
881         if isinstance(f.value, TryStableException):
882             f.trap(TryStableException)
883             self.set_gui('error_try_stable', str(f.value), [], False)
884
885         elif isinstance(f.value, TryDefaultMirrorException):
886             f.trap(TryDefaultMirrorException)
887             self.set_gui('error_try_default_mirror', str(f.value), [], False)
888
889         elif isinstance(f.value, DownloadErrorException):
890             f.trap(DownloadErrorException)
891             self.set_gui('error', str(f.value), [], False)
892
893         elif isinstance(f.value, DNSLookupError):
894             f.trap(DNSLookupError)
895             if common.settings['mirror'] != common.default_mirror:
896                 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)
897             else:
898                 self.set_gui('error', str(f.value), [], False)
899
900         elif isinstance(f.value, ResponseFailed):
901             for reason in f.value.reasons:
902                 if isinstance(reason.value, OpenSSL.SSL.Error):
903                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
904                     if not self.common.settings['update_over_tor']:
905                         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)
906                     else:
907                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
908
909         else:
910             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
911
912         self.build_ui()
913
914     def download(self, name, url, path):
915         # keep track of current download
916         self.current_download_path = path
917
918         # initialize the progress bar
919         mirror_url = url.format(self.common.settings['mirror'])
920         self.progressbar.set_fraction(0)
921         self.progressbar.set_text(_('Downloading {0}').format(name))
922         self.progressbar.show()
923         self.refresh_gtk()
924
925         # default mirror gets certificate pinning, only for requests that use the mirror
926         if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
927             agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
928         else:
929             agent = Agent(reactor)
930
931         # actually, agent needs to follow redirect
932         agent = RedirectAgent(agent)
933
934         # start the request
935         d = agent.request('GET', mirror_url,
936                           Headers({'User-Agent': ['torbrowser-launcher']}),
937                           None)
938
939         self.file_download = open(path, 'w')
940         d.addCallback(self.response_received).addErrback(self.download_error)
941
942         if not reactor.running:
943             reactor.run()
944
945     def try_stable(self, widget, data=None):
946         # change preferred to stable and relaunch TBL
947         self.common.settings['preferred'] = 'stable'
948         self.common.save_settings()
949         p = subprocess.Popen([self.common.paths['tbl_bin']])
950         self.destroy(False)
951
952     def try_default_mirror(self, widget, data=None):
953         # change preferred to stable and relaunch TBL
954         self.common.settings['mirror'] = self.common.default_mirror
955         self.common.save_settings()
956         p = subprocess.Popen([self.common.paths['tbl_bin']])
957         self.destroy(False)
958
959     def try_tor(self, widget, data=None):
960         # set update_over_tor to true and relaunch TBL
961         self.common.settings['update_over_tor'] = True
962         self.common.save_settings()
963         p = subprocess.Popen([self.common.paths['tbl_bin']])
964         self.destroy(False)
965
966     def attempt_update(self):
967         # load the update check file
968         try:
969             versions = json.load(open(self.common.paths['update_check_file']))
970             latest_stable = None
971             latest_alpha = None
972
973             # filter linux versions
974             valid_alphas = []
975             valid_stables = []
976             for version in versions:
977                 if '-Linux' in version:
978                     if 'alpha' in version or 'beta' in version or '-rc' in version:
979                         valid_alphas.append(str(version))
980                     else:
981                         valid_stables.append(str(version))
982             valid_alphas.sort()
983             if len(valid_alphas):
984                 latest_alpha = valid_alphas.pop()
985             valid_stables.sort()
986             if len(valid_stables):
987                 latest_stable = valid_stables.pop()
988
989             if latest_stable or latest_alpha:
990                 if latest_stable:
991                     self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
992                 if latest_alpha:
993                     self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
994                 self.common.settings['last_update_check_timestamp'] = int(time.time())
995                 self.common.settings['check_for_updates'] = False
996                 self.common.save_settings()
997                 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
998                 self.start_launcher()
999
1000             else:
1001                 # failed to find the latest version
1002                 self.set_gui('error', _("Error checking for updates."), [], False)
1003
1004         except:
1005             # not a valid JSON object
1006             self.set_gui('error', _("Error checking for updates."), [], False)
1007
1008         # now start over
1009         self.clear_ui()
1010         self.build_ui()
1011
1012     def verify(self):
1013         latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1014
1015         # initialize the progress bar
1016         self.progressbar.set_fraction(0)
1017         self.progressbar.set_text(_('Verifying Signature'))
1018         self.progressbar.show()
1019
1020         verified = False
1021         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1022         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1023         self.pulse_until_process_exits(p)
1024         if p.returncode == 0:
1025             # compare with sha256 of the tarball
1026             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1027             for line in open(self.common.paths['sha256_file'], 'r').readlines():
1028                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1029                     verified = True
1030
1031         if verified:
1032             self.run_task()
1033         else:
1034             # TODO: add the ability to report attack by posting bug to trac.torproject.org
1035             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)
1036             self.clear_ui()
1037             self.build_ui()
1038
1039             if not reactor.running:
1040                 reactor.run()
1041
1042     def extract(self):
1043         # initialize the progress bar
1044         self.progressbar.set_fraction(0)
1045         self.progressbar.set_text(_('Installing'))
1046         self.progressbar.show()
1047         self.refresh_gtk()
1048
1049         extracted = False
1050         try:
1051             if self.common.paths['tarball_file'][-2:] == 'xz':
1052                 # if tarball is .tar.xz
1053                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1054                 tf = tarfile.open(fileobj=xz)
1055                 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1056                 extracted = True
1057             else:
1058                 # if tarball is .tar.gz
1059                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1060                     tf = tarfile.open(self.common.paths['tarball_file'])
1061                     tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1062                     extracted = True
1063         except:
1064             pass
1065
1066         if not extracted:
1067             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1068             self.clear_ui()
1069             self.build_ui()
1070             return
1071
1072         # installation is finished, so save installed_version
1073         self.common.settings['installed_version'] = self.common.settings['latest_version']
1074         self.common.save_settings()
1075
1076         self.run_task()
1077
1078     def run(self, run_next_task=True):
1079         subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1080
1081         # play modem sound?
1082         if self.common.settings['modem_sound']:
1083             import pygame
1084             pygame.mixer.init()
1085             sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1086             sound.play()
1087             time.sleep(10)
1088
1089         if run_next_task:
1090             self.run_task()
1091
1092     # make the progress bar pulse until process p (a Popen object) finishes
1093     def pulse_until_process_exits(self, p):
1094         while p.poll() is None:
1095             time.sleep(0.01)
1096             self.progressbar.pulse()
1097             self.refresh_gtk()
1098
1099     # start over and download TBB again
1100     def start_over(self):
1101         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1102         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1103         self.gui_task_i = 0
1104         self.start(None)
1105
1106     # refresh gtk
1107     def refresh_gtk(self):
1108         while gtk.events_pending():
1109             gtk.main_iteration(False)
1110
1111     # exit
1112     def delete_event(self, widget, event, data=None):
1113         return False
1114
1115     def destroy(self, widget, data=None):
1116         if hasattr(self, 'file_download'):
1117             self.file_download.close()
1118         if hasattr(self, 'current_download_path'):
1119             os.remove(self.current_download_path)
1120             delattr(self, 'current_download_path')
1121         if reactor.running:
1122             reactor.stop()
1123
1124 if __name__ == "__main__":
1125     tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').read().strip()
1126
1127     print _('Tor Browser Launcher')
1128     print _('By Micah Lee, licensed under GPLv3')
1129     print _('version {0}').format(tor_browser_launcher_version)
1130     print 'https://github.com/micahflee/torbrowser-launcher'
1131
1132     common = TBLCommon(tor_browser_launcher_version)
1133
1134     # is torbrowser-launcher already running?
1135     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1136     if tbl_pid:
1137         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1138         common.bring_window_to_front(tbl_pid)
1139         sys.exit()
1140
1141     if '-settings' in sys.argv:
1142         # settings mode
1143         app = TBLSettings(common)
1144
1145     else:
1146         # launcher mode
1147         app = TBLLauncher(common)