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