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