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