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