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