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