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