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