]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
Merge pull request #81 from lazlolazlolazlo/patch-2
[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 class TryStableException(Exception):
58     pass
59 class TryDefaultMirrorException(Exception):
60     pass
61 class DownloadErrorException(Exception):
62     pass
63
64 class VerifyTorProjectCert(ClientContextFactory):
65
66     def __init__(self, torproject_pem):
67         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
68
69     def getContext(self, host, port):
70         ctx = ClientContextFactory.getContext(self)
71         ctx.set_verify_depth(0)
72         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
73         return ctx
74
75     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
76         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
77
78 class TBLCommon:
79
80     def __init__(self, tbl_version):
81         print _('Initializing Tor Browser Launcher')
82         self.tbl_version = tbl_version
83
84         # initialize the app
85         self.available_versions = {
86             'stable': _('Tor Browser Bundle - stable'),
87             'alpha': _('Tor Browser Bundle - alpha')
88         }
89         self.default_mirror = 'https://www.torproject.org/dist/'
90
91         self.discover_arch_lang()
92         self.build_paths()
93         self.mkdir(self.paths['data_dir'])
94         self.load_mirrors()
95         self.load_settings()
96         self.mkdir(self.paths['download_dir'])
97         self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
98         self.init_gnupg()
99
100         # allow buttons to have icons
101         try:
102             gtk_settings = gtk.settings_get_default()
103             gtk_settings.props.gtk_button_images = True
104         except:
105             pass
106
107     # discover the architecture and language
108     def discover_arch_lang(self):
109         # figure out the architecture
110         (sysname, nodename, release, version, machine) = os.uname()
111         self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
112
113         # figure out the language
114         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
115         default_locale = locale.getdefaultlocale()[0]
116         if default_locale == None:
117             self.language = 'en-US'
118         else:
119             self.language = default_locale.replace('_', '-')
120             if self.language not in available_languages:
121                 self.language = self.language.split('-')[0]
122                 if self.language not in available_languages:
123                     for l in available_languages:
124                         if l[0:2] == self.language:
125                             self.language = l
126             # if language isn't available, default to english
127             if self.language not in available_languages:
128                 self.language = 'en-US'
129
130     # build all relevant paths
131     def build_paths(self, tbb_version = None):
132         homedir = os.getenv('HOME')
133         if not homedir:
134             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
135             if os.path.exists(homedir) == False:
136                 try:
137                     os.mkdir(homedir, 0700)
138                 except:
139                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
140         if not os.access(homedir, os.W_OK):
141             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
142
143         tbb_data = '%s/.torbrowser' % homedir
144
145         if tbb_version:
146             # tarball filename
147             dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
148             if self.architecture == 'x86_64':
149                 arch = 'linux64'
150             else:
151                 arch = 'linux32'
152             tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
153
154             # tarball
155             self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
156
157             # sig
158             self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
159             self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
160             self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
161             self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mikeperry.asc'
162
163             self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
164             self.paths['tarball_filename'] = tarball_filename
165
166         else:
167             self.paths = {
168                 'tbl_bin': '/usr/bin/torbrowser-launcher',
169                 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
170                 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
171                 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
172                 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
173                 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
174                 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
175                 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
176                                 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
177                 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
178                 'data_dir': tbb_data,
179                 'download_dir': tbb_data+'/download',
180                 'gnupg_homedir': tbb_data+'/gnupg_homedir',
181                 'settings_file': tbb_data+'/settings',
182                 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
183                 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
184                 'tbb': {
185                     'stable': {
186                         'dir': tbb_data+'/tbb/stable/'+self.architecture,
187                         'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
188                         'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Browser/firefox',
189                         'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
190                         'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
191                     },
192                     'alpha': {
193                         'dir': tbb_data+'/tbb/alpha/'+self.architecture,
194                         'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
195                         'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Browser/firefox',
196                         'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
197                         'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
198                     }
199                 }
200             }
201
202     # create a directory
203     def mkdir(self, path):
204         try:
205             if os.path.exists(path) == False:
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     def get_pid(self, bin_path, python = False):
299         pid = None
300
301         for p in psutil.process_iter():
302             try:
303                 if p.pid != os.getpid():
304                     exe = None
305                     if python:
306                         if len(p.cmdline) > 1:
307                             if 'python' in p.cmdline[0]:
308                                 exe = p.cmdline[1]
309                     else:
310                         if len(p.cmdline) > 0:
311                             exe = p.cmdline[0]
312
313                     if exe == bin_path:
314                         pid = p.pid
315
316             except:
317                 pass
318
319         return pid
320
321     # bring program's x window to front
322     def bring_window_to_front(self, pid):
323         # figure out the window id
324         win_id = None
325         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
326         for line in p.stdout.readlines():
327             line_split = line.split()
328             cur_win_id = line_split[0]
329             cur_win_pid = int(line_split[2])
330             if cur_win_pid == pid:
331                 win_id = cur_win_id
332
333         # bring to front
334         if win_id:
335             subprocess.call(['wmctrl', '-i', '-a', win_id])
336
337 class TBLSettings:
338     def __init__(self, common):
339         print _('Starting settings dialog')
340         self.common = common
341
342         # set up the window
343         self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
344         self.window.set_title(_("Tor Browser Launcher Settings"))
345         self.window.set_icon_from_file(self.common.paths['icon_file'])
346         self.window.set_position(gtk.WIN_POS_CENTER)
347         self.window.set_border_width(10)
348         self.window.connect("delete_event", self.delete_event)
349         self.window.connect("destroy", self.destroy)
350
351         # build the rest of the UI
352         self.box = gtk.VBox(False, 10)
353         self.window.add(self.box)
354         self.box.show()
355
356         self.hbox = gtk.HBox(False, 10)
357         self.box.pack_start(self.hbox, True, True, 0)
358         self.hbox.show()
359
360         self.settings_box = gtk.VBox(False, 10)
361         self.hbox.pack_start(self.settings_box, True, True, 0)
362         self.settings_box.show()
363
364         self.labels_box = gtk.VBox(False, 10)
365         self.hbox.pack_start(self.labels_box, True, True, 0)
366         self.labels_box.show()
367
368         # preferred version
369         self.preferred_box = gtk.HBox(False, 10)
370         self.settings_box.pack_start(self.preferred_box, True, True, 0)
371         self.preferred_box.show()
372
373         self.preferred_label = gtk.Label(_('I prefer'))
374         self.preferred_label.set_line_wrap(True)
375         self.preferred_box.pack_start(self.preferred_label, True, True, 0)
376         self.preferred_label.show()
377
378         self.preferred_options = []
379         for i in self.common.available_versions:
380             self.preferred_options.append(self.common.available_versions[i])
381         self.preferred_options.sort()
382
383         self.preferred = gtk.combo_box_new_text()
384         for option in self.preferred_options:
385             self.preferred.append_text(option)
386         if self.common.settings['preferred'] in self.common.available_versions:
387             self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
388         else:
389             self.preferred.set_active(0)
390         self.preferred_box.pack_start(self.preferred, True, True, 0)
391         self.preferred.show()
392
393         # download over tor
394         # this feature isn't implemented yet (#8, #41), so commenting out the GUI for it
395         """
396         self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
397         self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
398         if self.common.settings['update_over_tor']:
399             self.tor_update_checkbox.set_active(True)
400         else:
401             self.tor_update_checkbox.set_active(False)
402         self.tor_update_checkbox.show()
403         """
404
405         # check for updates
406         self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
407         self.settings_box.pack_start(self.update_checkbox, True, True, 0)
408         if self.common.settings['check_for_updates']:
409             self.update_checkbox.set_active(True)
410         else:
411             self.update_checkbox.set_active(False)
412         self.update_checkbox.show()
413
414         # modem sound
415         self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
416         self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
417         if self.common.settings['modem_sound']:
418             self.modem_checkbox.set_active(True)
419         else:
420             self.modem_checkbox.set_active(False)
421         self.modem_checkbox.show()
422
423         # labels
424         if(self.common.settings['installed_version'][self.common.settings['preferred']]):
425             self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
426         else:
427             self.label1 = gtk.Label(_('Not installed'))
428         self.label1.set_line_wrap(True)
429         self.labels_box.pack_start(self.label1, True, True, 0)
430         self.label1.show()
431
432         if(self.common.settings['last_update_check_timestamp'] > 0):
433             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']))))
434         else:
435             self.label1 = gtk.Label(_('Never checked for updates'))
436         self.label1.set_line_wrap(True)
437         self.labels_box.pack_start(self.label1, True, True, 0)
438         self.label1.show()
439
440         # mirrors
441         self.mirrors_box = gtk.HBox(False, 10)
442         self.box.pack_start(self.mirrors_box, True, True, 0)
443         self.mirrors_box.show()
444
445         self.mirrors_label = gtk.Label(_('Mirror'))
446         self.mirrors_label.set_line_wrap(True)
447         self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
448         self.mirrors_label.show()
449
450         self.mirrors = gtk.combo_box_new_text()
451         for mirror in self.common.mirrors:
452             self.mirrors.append_text(mirror)
453         if self.common.settings['mirror'] in self.common.mirrors:
454             self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
455         else:
456             self.preferred.set_active(0)
457         self.mirrors_box.pack_start(self.mirrors, True, True, 0)
458         self.mirrors.show()
459
460         # button box
461         self.button_box = gtk.HButtonBox()
462         self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
463         self.box.pack_start(self.button_box, True, True, 0)
464         self.button_box.show()
465
466         # save and launch button
467         save_launch_image = gtk.Image()
468         save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
469         self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
470         self.save_launch_button.set_image(save_launch_image)
471         self.save_launch_button.connect("clicked", self.save_launch, None)
472         self.button_box.add(self.save_launch_button)
473         self.save_launch_button.show()
474
475         # save and exit button
476         save_exit_image = gtk.Image()
477         save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
478         self.save_exit_button = gtk.Button(_("Save & Exit"))
479         self.save_exit_button.set_image(save_exit_image)
480         self.save_exit_button.connect("clicked", self.save_exit, None)
481         self.button_box.add(self.save_exit_button)
482         self.save_exit_button.show()
483
484         # cancel button
485         cancel_image = gtk.Image()
486         cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
487         self.cancel_button = gtk.Button(_("Cancel"))
488         self.cancel_button.set_image(cancel_image)
489         self.cancel_button.connect("clicked", self.destroy, None)
490         self.button_box.add(self.cancel_button)
491         self.cancel_button.show()
492
493         # show the window
494         self.window.show()
495
496         # start gtk
497         gtk.main()
498
499     # save and launch
500     def save_launch(self, widget, data=None):
501         self.save()
502         p = subprocess.Popen([self.common.paths['tbl_bin']])
503         self.destroy(False)
504
505     # save and exit
506     def save_exit(self, widget, data=None):
507         self.save()
508         self.destroy(False)
509
510     # save settings
511     def save(self):
512         # figure out the selected preferred option
513         preferred = None
514         selected = self.preferred_options[self.preferred.get_active()]
515         for i in self.common.available_versions:
516             if self.common.available_versions[i] == selected:
517                 preferred = i
518         if preferred:
519             self.common.settings['preferred'] = preferred
520
521         # checkbox options
522         #self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
523         self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
524         self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
525
526         # figure out the selected mirror
527         self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
528
529         # save them
530         self.common.save_settings()
531
532     # exit
533     def delete_event(self, widget, event, data=None):
534         return False
535     def destroy(self, widget, data=None):
536         gtk.main_quit()
537
538
539 class TBLLauncher:
540     def __init__(self, common):
541         print _('Starting launcher dialog')
542         self.common = common
543
544         # init launcher
545         self.set_gui(None, '', [])
546         self.launch_gui = True
547         self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
548
549         # is firefox already running?
550         if self.common.settings['installed_version']:
551             firefox_pid = self.common.get_pid('./Browser/firefox')
552             if firefox_pid:
553                 print _('Firefox are is open, bringing to focus')
554                 # bring firefox to front
555                 self.common.bring_window_to_front(firefox_pid)
556                 return
557
558         # check for updates?
559         check_for_updates = False
560         if self.common.settings['check_for_updates']:
561             check_for_updates = True
562
563         if not check_for_updates:
564             # how long was it since the last update check?
565             # 86400 seconds = 24 hours
566             current_timestamp = int(time.time())
567             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
568                 check_for_updates = True
569
570         if check_for_updates:
571             # check for update
572             print 'Checking for update'
573             self.set_gui('task', _("Checking for Tor Browser update."),
574                 ['download_update_check',
575                  'attempt_update'])
576         else:
577             # no need to check for update
578             print _('Checked for update within 24 hours, skipping')
579             self.start_launcher()
580
581         if self.launch_gui:
582             # set up the window
583             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
584             self.window.set_title(_("Tor Browser"))
585             self.window.set_icon_from_file(self.common.paths['icon_file'])
586             self.window.set_position(gtk.WIN_POS_CENTER)
587             self.window.set_border_width(10)
588             self.window.connect("delete_event", self.delete_event)
589             self.window.connect("destroy", self.destroy)
590
591             # build the rest of the UI
592             self.build_ui()
593
594     # download or run TBB
595     def start_launcher(self):
596         # is TBB already installed?
597         latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
598         installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
599
600         # verify installed version for newer versions of TBB (#58)
601         if installed_version >= '3.0':
602             versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
603             if os.path.exists(versions_filename):
604                 for line in open(versions_filename):
605                     if 'TORBROWSER_VERSION' in line:
606                         installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
607
608         start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
609         if os.path.isfile(start) and os.access(start, os.X_OK):
610             if installed_version == latest_version:
611                 print _('Latest version of TBB is installed, launching')
612                 # current version of tbb is installed, launch it
613                 self.run(False)
614                 self.launch_gui = False
615             elif installed_version < latest_version:
616                 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
617                 # there is a tbb upgrade available
618                 self.set_gui('task', _("Your Tor Browser is out of date."),
619                     ['download_sha256',
620                      'download_sha256_sig',
621                      'download_tarball',
622                      'verify',
623                      'extract',
624                      'run'])
625             else:
626                 # for some reason the installed tbb is newer than the current version?
627                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
628
629         # not installed
630         else:
631             print _('TBB is not installed, attempting to install {0}'.format(latest_version))
632             self.set_gui('task', _("Downloading and installing Tor Browser."),
633                 ['download_sha256',
634                  'download_sha256_sig',
635                  'download_tarball',
636                  'verify',
637                  'extract',
638                  'run'])
639
640     # there are different GUIs that might appear, this sets which one we want
641     def set_gui(self, gui, message, tasks, autostart=True):
642         self.gui = gui
643         self.gui_message = message
644         self.gui_tasks = tasks
645         self.gui_task_i = 0
646         self.gui_autostart = autostart
647
648     # set all gtk variables to False
649     def clear_ui(self):
650         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
651             self.box.destroy()
652         self.box = False
653
654         self.label = False
655         self.progressbar = False
656         self.button_box = False
657         self.start_button = False
658         self.exit_button = False
659
660     # build the application's UI
661     def build_ui(self):
662         self.clear_ui()
663
664         self.box = gtk.VBox(False, 20)
665         self.window.add(self.box)
666
667         if 'error' in self.gui:
668             # labels
669             self.label = gtk.Label( self.gui_message )
670             self.label.set_line_wrap(True)
671             self.box.pack_start(self.label, True, True, 0)
672             self.label.show()
673
674             # button box
675             self.button_box = gtk.HButtonBox()
676             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
677             self.box.pack_start(self.button_box, True, True, 0)
678             self.button_box.show()
679
680             if self.gui != 'error':
681                 # yes button
682                 yes_image = gtk.Image()
683                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
684                 self.yes_button = gtk.Button("Yes")
685                 self.yes_button.set_image(yes_image)
686                 if self.gui == 'error_try_stable':
687                     self.yes_button.connect("clicked", self.try_stable, None)
688                 elif self.gui == 'error_try_default_mirror':
689                     self.yes_button.connect("clicked", self.try_default_mirror, None)
690                 elif self.gui == 'error_try_tor':
691                     self.yes_button.connect("clicked", self.try_tor, None)
692                 self.button_box.add(self.yes_button)
693                 self.yes_button.show()
694
695             # exit button
696             exit_image = gtk.Image()
697             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
698             self.exit_button = gtk.Button("Exit")
699             self.exit_button.set_image(exit_image)
700             self.exit_button.connect("clicked", self.destroy, None)
701             self.button_box.add(self.exit_button)
702             self.exit_button.show()
703
704         elif self.gui == 'task':
705             # label
706             self.label = gtk.Label( self.gui_message )
707             self.label.set_line_wrap(True)
708             self.box.pack_start(self.label, True, True, 0)
709             self.label.show()
710
711             # progress bar
712             self.progressbar = gtk.ProgressBar(adjustment=None)
713             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
714             self.progressbar.set_pulse_step(0.01)
715             self.box.pack_start(self.progressbar, True, True, 0)
716
717             # button box
718             self.button_box = gtk.HButtonBox()
719             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
720             self.box.pack_start(self.button_box, True, True, 0)
721             self.button_box.show()
722
723             # start button
724             start_image = gtk.Image()
725             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
726             self.start_button = gtk.Button(_("Start"))
727             self.start_button.set_image(start_image)
728             self.start_button.connect("clicked", self.start, None)
729             self.button_box.add(self.start_button)
730             if not self.gui_autostart:
731               self.start_button.show()
732
733             # exit button
734             exit_image = gtk.Image()
735             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
736             self.exit_button = gtk.Button(_("Exit"))
737             self.exit_button.set_image(exit_image)
738             self.exit_button.connect("clicked", self.destroy, None)
739             self.button_box.add(self.exit_button)
740             self.exit_button.show()
741
742         self.box.show()
743         self.window.show()
744
745         if self.gui_autostart:
746             self.start(None)
747
748     # start button clicked, begin tasks
749     def start(self, widget, data=None):
750         # disable the start button
751         if self.start_button:
752             self.start_button.set_sensitive(False)
753
754         # start running tasks
755         self.run_task()
756
757     # run the next task in the task list
758     def run_task(self):
759         self.refresh_gtk()
760
761         if self.gui_task_i >= len(self.gui_tasks):
762             self.destroy(False)
763             return
764
765         task = self.gui_tasks[self.gui_task_i]
766
767         # get ready for the next task
768         self.gui_task_i += 1
769
770         print _('Running task: {0}'.format(task))
771         if task == 'download_update_check':
772             print _('Downloading'), self.common.paths['update_check_url']
773             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
774
775         if task == 'attempt_update':
776             print _('Checking to see if update is needed')
777             self.attempt_update()
778
779         elif task == 'download_sha256':
780             print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
781             self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
782
783         elif task == 'download_sha256_sig':
784             print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
785             self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
786
787         elif task == 'download_tarball_sig':
788             print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
789             self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_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         p = 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         p = 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         latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
1005
1006         # initialize the progress bar
1007         self.progressbar.set_fraction(0)
1008         self.progressbar.set_text(_('Verifying Signature'))
1009         self.progressbar.show()
1010
1011         verified = False
1012         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1013         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1014         self.pulse_until_process_exits(p)
1015         if p.returncode == 0:
1016             # compare with sha256 of the tarball
1017             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1018             for line in open(self.common.paths['sha256_file'], 'r').readlines():
1019                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1020                     verified = True
1021
1022         if verified:
1023             self.run_task()
1024         else:
1025             # TODO: add the ability to report attack by posting bug to trac.torproject.org
1026             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)
1027             self.clear_ui()
1028             self.build_ui()
1029
1030             if not reactor.running:
1031                 reactor.run()
1032
1033     def extract(self):
1034         # initialize the progress bar
1035         self.progressbar.set_fraction(0)
1036         self.progressbar.set_text(_('Installing'))
1037         self.progressbar.show()
1038         self.refresh_gtk()
1039
1040         extracted = False
1041         try:
1042             if self.common.paths['tarball_file'][-2:] == 'xz':
1043                 # if tarball is .tar.xz
1044                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1045                 tf = tarfile.open(fileobj=xz)
1046                 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1047                 extracted = True
1048             else:
1049                 # if tarball is .tar.gz
1050                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1051                     tf = tarfile.open(self.common.paths['tarball_file'])
1052                     tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1053                     extracted = True
1054         except:
1055             pass
1056
1057         if not extracted:
1058             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1059             self.clear_ui()
1060             self.build_ui()
1061             return
1062
1063         # installation is finished, so save installed_version
1064         self.common.settings['installed_version'] = self.common.settings['latest_version']
1065         self.common.save_settings()
1066
1067         self.run_task()
1068
1069     def run(self, run_next_task = True):
1070         subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1071
1072         # play modem sound?
1073         if self.common.settings['modem_sound']:
1074             import pygame
1075             pygame.mixer.init()
1076             sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1077             sound.play()
1078             time.sleep(10)
1079
1080         if run_next_task:
1081             self.run_task()
1082
1083     # make the progress bar pulse until process p (a Popen object) finishes
1084     def pulse_until_process_exits(self, p):
1085         while p.poll() == None:
1086             time.sleep(0.01)
1087             self.progressbar.pulse()
1088             self.refresh_gtk()
1089
1090     # start over and download TBB again
1091     def start_over(self):
1092         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1093         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1094         self.gui_task_i = 0
1095         self.start(None)
1096
1097     # refresh gtk
1098     def refresh_gtk(self):
1099         while gtk.events_pending():
1100             gtk.main_iteration(False)
1101
1102     # exit
1103     def delete_event(self, widget, event, data=None):
1104         return False
1105     def destroy(self, widget, data=None):
1106         if hasattr(self, 'file_download'):
1107             self.file_download.close()
1108         if hasattr(self, 'current_download_path'):
1109             os.remove(self.current_download_path)
1110             delattr(self, 'current_download_path')
1111         if reactor.running:
1112             reactor.stop()
1113
1114 if __name__ == "__main__":
1115     tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').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)